1. Blog>
  2. Porting [druid] Rust Widgets to PineTime Smart Watch

Porting [druid] Rust Widgets to PineTime Smart Watch

by: Oct 13,2020 2588 Views 0 Comments Posted in Technology

Rust Internet of Things Gadgets Programming Nrf52 PineTime

A button that responds to our tapping and increments a counter… That’s what we shall accomplish today on the PineTime Smart Watch

The Watch App we see in the video was created with the [druid] Crate in Rust.

In [druid], UI controls are known as Widgets. Our app contains two Widgets: A Label Widget that displays the counter, and a Button Widget that increments the counter.

Lemme explain the code in the [druid] app

pub fn launch() {
  // Build a new window
  let main_window = WindowDesc::new(ui_builder);

From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/pinetime/rust/app/src/hello.rs

When the app is launched, we create a new window, passing it the ui_builder function that will return a list of Widgets and their layouts. (More about ui_builder later.)

// Application state is initially 0
  let data = 0_u32;

From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/pinetime/rust/app/src/hello.rs

Here we set the Application State to 0. What’s this Application State?

Remember the counter value that’s incremented every time we tap the button? This counter value is stored in the [druid] Application State. Widgets will interact with the Application State: Our Button Widget updates the state and our Label Widget displays the state.

That’s why [druid] is known as a Data-Oriented Rust UI… [druid] Widgets are bound to data values in the Application State.

  // Launch the window with the initial application state
  AppLauncher::with_window(main_window)
    .use_simple_logger()
    .launch(data)
    .expect("launch failed");
}

From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/pinetime/rust/app/src/hello.rs

Recall that main_window contains a list of Widgets and their layouts. We’ll pass this to the AppLauncher to launch the app window. The app shall be launched with data, the Application State, set to 0. If the launch fails, we’ll stop with an error launch failed.

Now let’s look at ui_builder and how it creates the Widgets…

/// Build the UI for the window. The application state consists of 1 value: `count` of type `u32`.
fn ui_builder() -> impl Widget<u32> // `u32` is the application state
  // Create a line of text based on a counter value
  let text =
    LocalizedString::new("hello-counter")
    .with_arg(
      "count", 
      // Closure that will fetch the counter value...
      | data: &u32, _env | // Closure will receive the application state and environment
        (*data).into()  // We return the counter value in the application state
    );

From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/pinetime/rust/app/src/hello.rs

First we create a text string that contains the counter value. How shall we get the value of the text string? Through this Rust Closure…

// Closure that will fetch the counter value...
| data: &u32, _env | // Closure will receive the application state and environment
  (*data).into()  // We return the counter value in the application state

From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/pinetime/rust/app/src/hello.rs

What’s a Rust Closure? Think of it as a Rust Function without a name. The Closure above is equivalent to this verbose Rust Function…

fn nameless_func(data: &u32, _env: …) -> … {
  (*data).into()
}

So it’s simpler to use the Closure form. The Closure simply returns the value of the counter in data (the Application State) as the value of the text string. The into part converts the returned value from number to string.

  // Create a label widget to display the text
  let label = Label::new(text);

From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/pinetime/rust/app/src/hello.rs

Now that we have the counter value stored in our text string, let’s display it with a Label Widget.

  // Create a button widget to increment the counter
  let button = Button::new(
    "increment", // Text to be shown
    // Closure that will be called when button is tapped...
    | _ctx, data, _env | // Closure will receive the context, application state and environment
      *data += 1    // We increment the counter
  );

From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/pinetime/rust/app/src/hello.rs

Here we create a button labelled increment that calls this Rust Closure whenever the button is tapped…

// Closure that will be called when button is tapped...
| _ctx, data, _env | // Closure will receive the context, application state and environment
  *data += 1    // We increment the counter

From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/pinetime/rust/app/src/hello.rs

This Closure is equivalent to the Rust Function…

fn nameless_func(_ctx: …, data: &mut u32, _env: …) {
  *data += 1
}

data contains our Application State, which is our counter value. The Closure simply increments our counter value by 1, every time the button is tapped.

Hey Presto! That’s the magic behind our Data-Oriented UI… The button increments the counter in our Application State, the label displays the value of the counter in our Application State!

[druid] completes the magic act by wiring up the Widgets to the Application State… When our Application State changes (upon tapping the button), [druid] automagically refreshes our Label Widget to show the new counter value.

Let’s tell [druid] how to layout our Label and Button Widgets for display…

  // Create a column for the UI
  let mut col = Column::new();
  // Add the label widget to the column, centered with padding
  col.add_child(
    Align::centered(
      Padding::new(5.0, label)
    ),
    1.0
  );

From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/pinetime/rust/app/src/hello.rs

We’ll display the Label and Button Widgets in a single column col. The Label Widget goes on top, centered horizontally, with a padding of 5 pixels.

  // Add the button widget to the column, with padding
  col.add_child(
    Padding::new(5.0, button), 
    1.0
  );

From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/pinetime/rust/app/src/hello.rs

The Button Widget appears next in col. Also with a padding of 5 pixels.

Note that the second argument to add_child is the same for both the label and the button: 1.0. This means that the label and button shall occupy equal vertical spacing, i.e. the label shall fill the top half of the screen, the button shall fill the bottom half. This is similar to the flexbox concept in CSS.

  // Return the column containing the label and button widgets
  col
}

From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/pinetime/rust/app/src/hello.rs

And lastly, we return the col to the caller. To recap: col contains the Label and Button Widgets, the Closures for displaying and incrementing the Application State counter, and the layout of the Widgets.

That’s the clever simplicity of [druid] apps. [druid] apps have a Declarative UI like SwiftUI and Flutter… Without the legacy baggage of iOS and Android. That’s why I think [druid] is perfect for building Watch Apps for PineTime!

Here’s the complete [druid] Watch App for PineTime…

#![no_std] // This program will run on embedded platforms
use druid::widget::{Align, Button, Column, Label, Padding};
use druid::{AppLauncher, LocalizedString, Widget, WindowDesc};

pub fn launch() {
  // Build a new window
  let main_window = WindowDesc::new(ui_builder);
  // Application state is initially 0
  let data = 0_u32;
  // Launch the window with the initial application state
  AppLauncher::with_window(main_window)
    .use_simple_logger()
    .launch(data)
    .expect("launch failed");
}

/// Build the UI for the window. The application state consists of 1 value: `count` of type `u32`.
fn ui_builder() -> impl Widget<u32> { // `u32` is the application state
  // Create a line of text based on a counter value
  let text =
    LocalizedString::new("hello-counter")
    .with_arg(
      "count", 
      // Closure that will fetch the counter value...
      | data: &u32, _env | // Closure will receive the application state and environment
        (*data).into()  // We return the counter value in the application state
    );
  // Create a label widget to display the text
  let label = Label::new(text);
  // Create a button widget to increment the counter
  let button = Button::new(
    "increment"// Text to be shown
    // Closure that will be called when button is tapped...
    | _ctx, data, _env | // Closure will receive the context, application state and environment
      *data += 1    // We increment the counter
  );

  // Create a column for the UI
  let mut col = Column::new();
  // Add the label widget to the column, centered with padding
  col.add_child(
    Align::centered(
      Padding::new(5.0, label)
    ),
    1.0
  );
  // Add the button widget to the column, with padding
  col.add_child(
    Padding::new(5.0, button), 
    1.0
  );
  // Return the column containing the label and button widgets
  col
}

From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/pinetime/rust/app/src/hello.rs


Powering [druid] with Type Inference

The above Rust source code for the watch app looks so trivial (compared with SwiftUI and Flutter)… Surely something must be missing in that app?

Oh yes, plenty is missing from the app… But the Rust Compiler performs Type Inference and fills in all the missing bits for us! Here’s how it looks with the missing bits filled in…

pub fn launch() {
  // Build a new window
  let main_window = WindowDesc::<u32,Flex<u32>>::new(ui_builder);
  // Application state is initially 0
  let data = 0_u32;
  // Launch the window with the initial application state
  AppLauncher::<u32,Flex<u32>>::with_window(main_window)
    .use_simple_logger()
    .launch(data)
    .expect("launch failed");
}

/// Build the UI for the window. The application state consists of 1 value: `count` of type `u32`.
fn ui_builder() -> Flex<u32> { // `u32` is the window state
  // Create a line of text based on a counter value
  let text =
    LocalizedString::<u32>::new("hello-counter")
    .with_arg(
      "count", 
      // Closure that will fetch the counter value...
      | data: &u32, _env | (*data).into()
    );
  // Create a label widget to display the text
  let label = Label::<u32>::new(text);
  // Create a button widget to increment the counter
  let button = Button::<u32>::new(
    "increment", 
    // Closure that will be called when button is tapped...
    | _ctx, data, _env | *data += 1
  );

  // Create a column for the UI
  let mut col = Column::new::<u32>();
  // Add the label widget to the column, centered with padding
  col.add_child::<Align::<u32>>(
    Align::<u32>::centered(
      Padding::<u32>::new(5.0, label)
    ),
    1.0
  );
  // Add the button widget to the column, with padding
  col.add_child::<Padding::<u32>>(
    Padding::<u32>::new(5.0, button), 
    1.0
  );
  // Return the column containing the label and button widgets
  col
}

Note that the Widgets have been expanded as Label<u32> and Button<u32>. That’s because Label and Button are actually Generic Types in Rust.

Quick recap of Generic Types: Vec is a Generic Type that represents a vector (array) of values. When we write Vec<i32>, it refers to a vector of integer values (i32).

Thus Label<u32> and Button<u32> are Generic Widgets that are backed by u32, our Application State that contains the counter. Reminds us that [druid] is really a Data-Oriented UI!

Some types (like WindowDesc) are Generic Types with two parameters. So this line of code in our app…

let main_window = WindowDesc::new(ui_builder);

Actually expands to this complicated mess…

let main_window = WindowDesc::<u32,Flex<u32>>::new(ui_builder);

What a bloody clever compiler! The Rust Compiler makes it easy for us to write Watch Apps. Under the hood, things are a lot more complicated with the Generic Types. Read on to learn how [druid] Widgets are implemented on PineTime…


Downsizing [druid] from Desktop to Embedded

[druid] is a GUI toolkit that produces desktop apps for Windows, macOS and Linux. It doesn’t support Embedded Platforms like PineTime. So I had to change some features to make [druid] work on PineTime. (Only the [druid] implementation was changed, not the [druid] APIs… The same [druid] desktop application code can be compiled for PineTime without any code changes! Compare the above [druid] code for PineTime with the original version)

On Embedded Rust we declare our programs as #![no_std]. This means that our embedded programs will call the Rust Core Library, instead of the usual Rust Standard Library which has many more features. Any code in [druid] that uses the following features from the Rust Standard Library will NOT compile on PineTime…

1.Vec vectors, because it uses Heap Memory to support resizable vectors. And Heap Memory is not available on embedded platforms (by default).

2.String (though &str is supported), because it also uses Heap Memory.

3.format!, because internally it uses a String to write formatted strings.

4.Box, RefCell, Arc (Atomically Reference Counted) and Cow (Clone On Write), because they use Heap Memory as well.

Why isn’t Heap Memory available on Embedded Rust? Embedded platforms like PineTime often have little RAM. (Only 64 KB RAM for PineTime’s Nordic nRF52832 microcontroller) When RAM is limited, we prefer to budget our memory requirements in advance… Just to be sure that our Smart Watch doesn’t allocate too many Widgets at runtime and crash.

Hence all space for Widgets should be preallocated in Static Memory before the Watch App starts. (There is a way to implement Heap Memory on embedded platforms, check this for details)

So many features are missing from Embedded Rust… Is it still possible to run [druid] apps on PineTime? Yes, I used some tricks to make a quick (and somewhat dirty) port of [druid] to PineTime… Which you can also use for porting Rust desktop libraries to embedded platforms! Read on to learn more…


Porting Vectors, Strings and format! to Embedded

The Vec and String types are used in many Rust libraries. On embedded platforms we can’t allow vectors and strings to be resized on demand… But we can create vectors and strings that have a maximum size.

Here’s the quickest (and dirtiest) way to use the [heapless] crate to replace standard types Vec and String by vectors and strings with fixed size limits…

/// Max vector size: 2
type MaxVectorLength = heapless::consts::U2;
/// Vec is now redefined as a fixed-size vector
type Vec<T> = heapless::Vec::<T, MaxVectorLength>;

/// Max length of strings: 20 characters
type MaxStringLength = heapless::consts::U20;
/// String is now redefined as a fixed-size string
type String = heapless::String::<MaxStringLength>;

From From https://github.com/lupyuen/druid-embedded/blob/master/druid/src/localization.rs#L87-L91

(Yep told you this would be dirty… Should never override standard Rust types!)

Most of the surrounding code that calls Vec and String should compile with this simple modification. (We may need to replace &str by String) Watch out for the extra copies of vectors and strings that our program will now be copying… Keep them small!

Once the code works on our embedded gadget, Do The Right Thing and rename Vec and String to something sensible, like FixedVec and FixedString.

There’s a similar trick for porting format! to embedded platforms. Let’s say we are converting a number v to a text string…

format!("{}", v)

This fails to compile because format! uses an internal String to store the formatted result. The [heapless] solution uses the write! macro like this…

/// Max length of strings: 20 characters
type MaxStringLength = heapless::consts::U20;
/// Declare a fixed-size string
type FixedString = heapless::String::<MaxStringLength>;

/// Buffer for the converted output
let mut buffer = FixedString::new();
// Convert v to a text string and store in the buffer
write!(&mut buffer, "{}", v)
  .expect("format fail");
buffer // Contains v converted to a string

From https://github.com/lupyuen/druid-embedded/blob/master/druid/src/argvalue.rs#L63-L66


Boxing up Widgets and Windows

Without Heap Memory, [druid] can’t use Box to allocate Widgets on the heap…

/// A widget container with either horizontal or vertical layout.
impl<T: Data> Flex<T> {
  /// Add a child widget.
  pub fn add_child(&mut self, child: impl Widget<T> + 'static, flex: f64) {
    let params = Params { flex };
    let child = ChildWidget {
      // Fails to compile on embedded! "boxed" not available
      widget: WidgetPod::new(child).boxed(),
      params,
    };
    self.children.push(child);
  }
}

Original [druid] code that allocates Widgets on the heap. From https://github.com/xi-editor/druid/blob/master/druid/src/widget/flex.rs#L103-L151

Why does [druid] use Box Widgets? Because [druid] has Container Widgets (Flex / Row / Column) that contain references to Child Widgets, like we see above.

In our embedded version, [druid] uses a custom WidgetBox type that pretends to be a Widget in a Box

/// A widget container with either horizontal or vertical layout.
impl<T: Data + 'static + Default> Flex<T> { ////
  /// Add a child widget. Now accepts a `Widget` Type (e.g. `Button<u32>`) as a Generic Parameter.
  /// Also `child` is now a concrete `Widget` Type instead of `impl Widget<T>`
  pub fn add_child<W: Widget<T> + Clone>(&mut self, child: W, flex: f64) {
    let params = Params { flex };
    let child = ChildWidget {
      widget: WidgetPod::new(
        // Store the widget statically. Return a `WidgetBox` that contains the widget index.
        WidgetBox::<T>::new(child)
      ),
      params,
    };
    self.children.push(child)
      .expect("add child fail");
  }
}

Modified [druid] code that uses static WidgetBox. From https://github.com/lupyuen/druid-embedded/blob/master/druid/src/widget/flex.rs#L126-L151

Here’s a crude implementation of WidgetBox… Instead of the heap, WidgetBox stores Widgets in an array in Static Memory. WidgetBox then returns the array index (Widget ID) at which the Widget is stored….

/// Max number of `Widgets` on embedded platforms
const MAX_WIDGETS: usize = 10;

/// Static list of `Widgets` just for embedded platforms. TODO: Clean up with `singleton`: https://docs.rs/cortex-m/0.4.2/cortex_m/macro.singleton.html
static mut WIDGET_STATE_U32: [ WidgetType<u32>; MAX_WIDGETS ] = [ WidgetType::None, ... ]; // TODO: Simplify with `proc-quote`: https://crates.io/crates/proc-quote

/// Enum to store each type of `Widget`. `D` is the data type of the Application State, e.g. `u32`
pub enum WidgetType<D: Data + 'static + Default> {
  None,
  Align(Align<D>),
  Button(Button<D>),
  Flex(Flex<D>),
  Label(Label<D>),
  Padding(Padding<D>),
}

/// Boxed version of a `Widget`, which contains only the ID, not the actual `Widget`
pub struct WidgetBox<D: Data + 'static>(
  u32,       // Widget ID, the index of `WIDGET_STATE_U32` at which the `Widget` is stored
  PhantomData<D>, // Needed to do compile-time checking for `Data`
);

/// Generic implementation of `WidgetBox`
impl<D: Data + 'static + Default> WidgetBox<D> {
  /// Create a new box for the `Widget` with type `W` (e.g. `Button<u32>`)
  pub fn new<W: Widget<D> + Clone>(widget: W) -> Self {
    let id = widget.clone().get_id();
    // Wrap the `Widget` in a `WidgetType` enum
    let widget_type: WidgetType<D> = widget.to_type();
    // Return a `WidgetBox` that contains the ID
    let widget_box: WidgetBox<D> = WidgetBox(
      id,
      PhantomData,
    );
    // Store the `WidgetType` in the static array
    widget_box.clone().add_widget(widget_type);
    widget_box
  }
}

From https://github.com/lupyuen/druid-embedded/blob/master/druid/src/widget/widgetbox.rs

WidgetBox implements the Traits (features) of a Widget by forwarding the function calls to the Widget stored in Static Memory…

/// Implementation of `Widget` trait for `WidgetBox`. We just forward to the inner `Widget`.
impl<D: Data + 'static + Default> Widget<D> for WidgetBox<D> {
  /// Paint the `Widget`
  fn paint(
    &mut self, 
    paint_ctx: &mut PaintCtx, 
    base_state: &BaseState, 
    data: &D, 
    env: &Env
  ) {
    match &mut self.get_widgets()[self.0 as usize] {
      // TODO: Simplify with `ambassador` https://github.com/hobofan/ambassador
      WidgetType::Align(w)  => w.paint(paint_ctx, base_state, data, env),
      WidgetType::Button(w) => w.paint(paint_ctx, base_state, data, env),
      WidgetType::Flex(w)  => w.paint(paint_ctx, base_state, data, env),
      WidgetType::Label(w)  => w.paint(paint_ctx, base_state, data, env),
      WidgetType::Padding(w) => w.paint(paint_ctx, base_state, data, env),
      WidgetType::None    => panic!("missing widget")
    };
  }
  ...

From https://github.com/lupyuen/druid-embedded/blob/master/druid/src/widget/widgetbox.rs

Storing Widgets statically can be tricky… Our Widgets are Generic Types that depend on the data type of the Application State. Earlier we have seen that the Rust Compiler expands our Watch App with labels and buttons as Label<u32> and Button<u32>

The solution is to use a Specialised Trait like this…

/// Specialised Trait for handling static `Widgets` on embedded platforms
pub trait GlobalWidgets<D: Data + 'static + Default> {
  /// Fetch the static `Widgets` for the Data type
  fn get_widgets(&self) -> &'static mut [ WidgetType<D> ];
  /// Add a `Widget` for the Data type
  fn add_widget(&self, widget: WidgetType<D>);
}

/// Default Trait will not have static `Widgets`
impl<D: Data + 'static + Default> GlobalWidgets<D> for WidgetBox<D> {
  default fn get_widgets(&self) -> &'static mut [ WidgetType<D> ] { panic!("no global widgets") }
  default fn add_widget(&self, _widget: WidgetType<D>) { panic!("no global widgets") }
}

/// Specialised Trait will store `Widgets` statically on embedded platforms
impl GlobalWidgets<u32> for WidgetBox<u32> {
  /// Fetch the static `Widgets` for the Data type
  fn get_widgets(&self) -> &'static mut [ WidgetType<u32> ] {
    unsafe { &mut WIDGET_STATE_U32 }
  }
  /// Add a `Widget` for the Data type
  fn add_widget(&self, widget: WidgetType<u32>) {
    assert!(self.0 < MAX_WIDGETS as u32, "too many widgets");
    unsafe { WIDGET_STATE_U32[self.0 as usize] = widget; }     
  }   
}

From https://github.com/lupyuen/druid-embedded/blob/master/druid/src/widget/widgetbox.rs

Note the pattern for coding a Specialised Trait…

1.Declare the Trait across all types:

trait GlobalWidgets<D> { fn f(arg: D); ...

2.Implement the Default Trait:

impl<D> GlobalWidgets<D> { default fn f(arg: D) { ...

3.Implement the Specialised Trait for each specific type like u32:

impl GlobalWidgets<u32> { fn f(arg: u32) { ...

The same trick is used to port [druid] Windows to PineTime. Whenever we need to Box up a [druid] Window, we use a WindowBox instead. Windows and Window Handlers are now stored in a static array just like Widgets.

[druid] Dependencies


Connect [druid] to PineTime’s Display and Touch Controllers

Now that [druid] compiles successfully on PineTime, let’s connect our fork of [druid] to the PineTime display and touch hardware…

lupyuen /druid-embedded

[druid] calls the [piet] crate to render graphics and text. In our first PineTime article, we have created a Rust display driver based on the [embedded-graphics] and [st7735-lcd] crates.

Hence the solution: Adapt [piet] to render graphics and text with [embedded-graphics]. Here’s the resulting fork…

lupyuen /piet-embedded

Here’s a peek of [piet] adapted for [embedded-graphics]: Rendering a Bezier path with [embedded-graphics] primitives (so that we can have rounded buttons)…

/// Render 2D graphics with embedded-graphics
impl RenderContext for EmbedRenderContext {
  /// Render a Bezier path with embedded-graphics
  fn stroke(&mut self, shape: impl Shape, brush: &impl IntoBrush<Self>, width: f64) {
    let brush = brush.make_brush(self, || shape.bounding_box());

    // Get stroke color
    let stroke = self.convert_brush(&brush);

    // Draw a line for each segment of the Bezier path
    let mut first: Option<Point> = None;
    let mut last = Point::ZERO;
    for el in shape.to_bez_path(0.1) { // Previously 1e-3
      match el {
        PathEl::MoveTo(p) => {
          if (first.is_none()) { first = Some(p); }
          last = p;
        }
        PathEl::LineTo(p) => {
          // Draw line from last to p with styled stroke
          let last_coord = Coord::new(last.x as i32, last.y as i32);
          let p_coord = Coord::new(p.x as i32, p.y as i32);
          let line = Line::<Rgb565>
            ::new(last_coord, p_coord)
            .stroke(Some(stroke))
            .stroke_width(width as u8)
            .translate(get_transform_stack());
          unsafe { display::DISPLAY.draw(line); }
          if (first.is_none()) { first = Some(p); }
          last = p;
        }     

Rendering Widgets with [embedded-graphics]. From https://github.com/lupyuen/piet-embedded/blob/master/piet-embedded-graphics/src/context.rs#L67-L199

The display driver from our first PineTime article display.rs has been relocated into the [piet] library.

Advanced Topic: The [embedded-graphics] version of [piet] make take a few seconds to blank the screen or fill a region with colour (e.g. for buttons). I may enhance the display driver [st7735-lcd] to use SPI with DMA for such operations.

What about touch? In our second PineTime article, we have created a Rust Touch Controller Driver. This Touch Controller Driver has been wired up to send touch events to [druid]. [druid] doesn’t support touch events yet, so we simulate the mouse down + mouse up events instead…

/// Handle a touch event at coordinates (x,y) by simulating a mouse press: mouse down then up
pub fn handle_touch(x: u16, y: u16) {
  let mut ctx = DruidContext::new();
  // Get the handler for the window
  let handler = unsafe { &mut ALL_HANDLERS_U32[1] }; // Assume first window has ID 1
  // Simulate a mouse down event
  handler.mouse_down(
    &MouseEvent {
      pos: Point::new(x as f64, y as f64),
      count: 1,
      button: MouseButton::Left,
    },
    &mut ctx,
  );
  // Simulate a mouse up event
  handler.mouse_up(
    &MouseEvent {
      pos: Point::new(x as f64, y as f64),
      count: 0,
      button: MouseButton::Left,
    },
    &mut ctx,
  );
}

/// ALL_HANDLERS[i] is the Window Handler for the Window with window ID i. i=0 is not used.
/// TODO: Clean up with `singleton`: https://docs.rs/cortex-m/0.4.2/cortex_m/macro.singleton.html
/// TODO: Simplify with `proc-quote`: https://crates.io/crates/proc-quote
static mut ALL_HANDLERS_U32: [ DruidHandler<u32>; MAX_WINDOWS ] = [ DruidHandler::<u32> { window_id: WindowId(0), phantom: PhantomData }, ... ];

Handling Touchable Widgets. From https://github.com/lupyuen/druid-embedded/blob/master/druid/src/win_handler.rs#L62-L90

Advanced Topic: If you check the video demo, most taps of the button respond within 1 second, but some taps on the button don’t seem to increment the counter, and some taps appear to increment the counter twice. That’s because the Touch Controller Driver has not been implemented completely.

The touchscreen controller supports multitouch, so the controller tracks each finger and reports whether each finger is a press (down) or release (up) action. The current Touch Controller Driver interprets only the first finger detected, and always assume it’s a press (down) action not a release (up) action.


Other Features Downsized in [druid] and [kurbo]

The above fork of [druid] has some features commented out…

1.Localization: Our [druid] app uses a Localized String hello-counter… This actually refers to a Resource File that specifies the displayed text as Current value is <count>. This is probably overkill for a proof-of-concept, so it has been stubbed out and replaced by a simpler version that displays only the first argument.

2.Themes and Environments: [druid] supports UI themes and configurable environment settings. For embedded platforms they should probably be implemented as compile-time constants. So Themes and Environments have been stubbed out for now.

3.Menus and Keyboards: Since they are not necessary for Watch Apps

[druid] and [piet] use the [kurbo] crate for computing Bezier curves, which are necessary for drawing rounded buttons. [kurbo] has been downsized to work with PineTime and #![no_std]

1.Bezier paths are limited to 16 points

2.Double-precision floating-point (f64) math functions seem to be missing from the Rust Core Library, so we are using the [libm] crate instead. We could switch to single-precision floating-point (f32) and use [micromath] instead, because it’s optimised for embedded platforms.

Here is my fork of [kurbo] for PineTime…

lupyuen /kurbo-embedded


PineTime ROM Usage for [druid] and Friends

[druid] + [piet] + [kurbo] + [embedded-graphics] + [st7735-lcd] + Rust Core Library + Mynewt OS… Will this long and exciting Conga Line even run on PineTime’s Nordic nRF52832 microcontroller with 512 KB ROM and 64 KB RAM?

Yes it does! Total ROM used is only 237 KB, less than half of the available ROM space!

And this was built in Debug Mode, not optimised Release Mode! (Which is hard to debug and learn)

What about ROM space for firmware upgrades? Well we have an additional 4 MB SPI Flash available on PineTime!

Total RAM used is 46 KB, which includes 32 KB of stack space (Probably too much)

So a functional Rust Widget UI for Watch Apps looks mighty feasible on PineTime!

I think it also helps when I replaced the Box references in [druid] by WidgetBox and WindowBox… The Rust Compiler does an incredible job of removing dead code when everything is statically defined.

We can analyse the ROM bloat by loading this Linker Output Map into a Memory Map Google Sheet that’s explained in this article

Functions that occupy the most ROM space, sorted by ROM size (in bytes). From https://docs.google.com/spreadsheets/d/1Lb217jqZGM7NlnOVCxpaLAI0CQcOTPnbMpksDSPVyiw/edit#gid=381366828&fvid=1643056349

The Google Sheet is here. It shows the functions that occupy the most ROM space, sorted by ROM size (in bytes). Some observations…

1.[libm], the floating-point math library, seems to be taking the most ROM space. We could replace this by [micromath], a smaller math library for embedded platforms

2.core::num::flt2dec::strategy needs closer study. And why dragon and his friend grisu are hiding in my watch!

[druid] seems to take a fair amount of ROM space. Which is expected since [druid] was designed for desktop use. The ROM usage will probably drop drastically once we enable size optimisation in the Rust Release Build.


PineTime build, flash and debug steps on Visual Studio Code


Note: The content and the pictures in this article are contributed by the author. The opinions expressed by contributors are their own and not those of PCBWay. If there is any infringement of content or pictures, please contact our editor (zoey@pcbway.com) for deleting.


Written by

Join us
Wanna be a dedicated PCBWay writer? We definately look forward to having you with us.
  • Comments(0)
You can only upload 1 files in total. Each file cannot exceed 2MB. Supports JPG, JPEG, GIF, PNG, BMP
0 / 10000
    Back to top